16.6 其他

从运行时的角度,整个进程内的对象可分为两类:一种,自然是从arena区域分配的用户对象;另一种,则是运行时自身运行和管理所需的对象,比如管理arena内存片段的mspan,提供无锁分配的mcache等等。

管理对象的生命周期并不像用户对象那样复杂,且类型和长度都相对固定,所以算法策略显然不用那么复杂。还有,它们相对较长的生命周期也不适合占用arena区域,否则会导致更多碎片。为此,运行时专门设计了FixAlloc固定分配器来为管理对象分配内存。

固定分配器使用相同的算法框架,只有相应参数不同。

mfixalloc.go

type fixalloc struct{ size uintptr // 固定分配长度 first unsafe.Pointer // 关联函数 arg unsafe.Pointer // 关联函数调用参数 list *mlink // 复用链表 chunk *byte // 内存块指针 nchunk uint32 // 内存块长度 inuse uintptr // 内存块已用长度 }

当运行时在初始化heap时,一共构建了4种固定分配器。

mheap.go

func mHeap_Init(h*mheap,spans_size uintptr) { fixAlloc_Init(&h.spanalloc,unsafe.Sizeof(mspan{}),recordspan, …) fixAlloc_Init(&h.cachealloc,unsafe.Sizeof(mcache{}),nil,nil, …) fixAlloc_Init(&h.specialfinalizeralloc,unsafe.Sizeof(specialfinalizer{}),nil, …) fixAlloc_Init(&h.specialprofilealloc,unsafe.Sizeof(specialprofile{}),nil, …) }

mfixalloc.go

func fixAlloc_Init(ffixalloc,size uintptr,first…,arg unsafe.Pointer,statuint64) { f.size=size f.first= *(*unsafe.Pointer)(unsafe.Pointer(&first)) f.arg=arg f.list=nil f.chunk=nil f.nchunk=0 f.inuse=0 f.stat=stat }

分配算法优先从复用链表获取内存,只在获取失败,或剩余空间不足时才获取新内存块。

mfixalloc.go

func fixAlloc_Alloc(f*fixalloc)unsafe.Pointer{ // 尝试从可用链表提取 if f.list!=nil{ v:=unsafe.Pointer(f.list) f.list=f.list.next f.inuse+=f.size return v }

// 如果剩余内存块已不足分配,则获取新内存块(16KB) if uintptr(f.nchunk) <f.size{ f.chunk= (*uint8)(persistentalloc(_FixAllocChunk,0,f.stat)) f.nchunk= _FixAllocChunk }

// 获取新内存块时执行关联函数(通常用作初始化和拷贝数据) v:= (unsafe.Pointer)(f.chunk) if f.first!=nil{ fn:= *(*func(unsafe.Pointer,unsafe.Pointer))(unsafe.Pointer(&f.first)) fn(f.arg,v) }

// 更新属性 f.chunk= (*byte)(add(unsafe.Pointer(f.chunk),f.size)) f.nchunk-=uint32(f.size) f.inuse+=f.size

return v }

固定分配器持有的这个16 KB内存块分自persistent区域。该区域在很多地方为运行时提供后备内存,目的同样是为了减少并发锁,减少内存申请系统调用。

malloc.go

type persistentAlloc struct{ base unsafe.Pointer off uintptr }

var globalAlloc struct{ persistentAlloc }

func persistentalloc(size,align uintptr,sysStat*uint64)unsafe.Pointer{ systemstack(func() { p=persistentalloc1(size,align,sysStat) }) return p }

func persistentalloc1(size,align uintptr,sysStat*uint64)unsafe.Pointer{ const( chunk =256<<10 maxBlock=64<<10 //VM reservation granularity is 64K on windows )

// 直接分配大于64KB的内存块 if size>=maxBlock{ return sysAlloc(size,sysStat) }

// 后备内存块存放位置(本地或全局) var persistent*persistentAlloc if mp!=nil&&mp.p!=0{ persistent= &mp.p.ptr().palloc }else{ persistent= &globalAlloc.persistentAlloc }

// 偏移位置对齐 persistent.off=round(persistent.off,align)

// 如果后备块空间不足,则重新申请 if persistent.off+size>chunk||persistent.base==nil{ // 申请新256KB后备内存 persistent.base=sysAlloc(chunk, &memstats.other_sys) persistent.off=0 }

// 截取所需内存块 p:=add(persistent.base,persistent.off) persistent.off+=size return p }

至于释放过程,只简单地放回复用链表即可。

mfixalloc.go

func fixAlloc_Free(f*fixalloc,p unsafe.Pointer) { f.inuse-=f.size v:= (*mlink)(p) v.next=f.list f.list=v }

recordspan

四个FixAlloc,只有mspan指定了关联函数recordspan,其作用是按需扩张h_allspans存储空间。h_allspans保存了所有span对象指针,供垃圾回收时遍历。

内存分配器spans区域虽然保存了page/span映射关系,但有很多重复,基于效率考虑,并不适合用来作为遍历对象。

mheap.go

var h_allspans[]*mspan

func mHeap_Init(h*mheap,spans_size uintptr) { fixAlloc_Init(&h.spanalloc,unsafe.Sizeof(mspan{}),recordspan, …) }

func recordspan(vh unsafe.Pointer,p unsafe.Pointer) { h:= (*mheap)(vh) s:= (*mspan)(p)

// 如果空间已满 … if len(h_allspans) >=cap(h_allspans) { // 计算新容量 n:=64*1024/ptrSize if n<cap(h_allspans)*3/2{ n=cap(h_allspans) *3/2 }

 // 申请新内存空间(直接用指针写slice内部属性) 
var new[]*mspan
sp:= (*slice)(unsafe.Pointer(&new)) 
sp.array=sysAlloc(uintptr(n)*ptrSize, &memstats.other_sys) 
sp.len=len(h_allspans) 
sp.cap=n

 // 如果原空间有数据,则复制后释放 
if len(h_allspans) >0{ 
     // 拷贝数据 
    copy(new,h_allspans) 

     // 释放旧内存块 
     // 或由gcSweep->gcCopySpans释放 
    if h.allspans!=mheap_.gcspans{ 
        sysFree(unsafe.Pointer(h.allspans), ...) 
     } 
 } 

 // 指向新空间 
h_allspans=new
h.allspans= (**mspan)(unsafe.Pointer(sp.array)) 

}

// 注意: // 上面的扩张直接用mmap在arena以外申请空间 // 而append引发的扩张是在arena区域 // 基于管理目的的h_allspans不适合用于arena区域

h_allspans=append(h_allspans,s) h.nspan=uint32(len(h_allspans)) }